﻿#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 ZHENG Zhong <heavyzheng nospam-at gmail D0T com>
# - http://heavyz.blogspot.com/
# - http://buggarden.blogspot.com/
#
# View functions for the forum webapp.
#
# Created on 2008-05-07.
# $Id: views.py 291 2008-12-03 10:17:53Z heavyzheng $
#

import logging
import datetime
import urllib

from django.template import loader
from django.template import Context
from django.http import HttpRequest
from django.http import HttpResponse
from django.http import HttpResponseRedirect
from django.http import Http404

from google.appengine.api import users

from buggarden.apps.forum.config import Config
from buggarden.apps.forum.models import ForumBoard
from buggarden.apps.forum.models import ForumAttachment
from buggarden.apps.forum.models import ForumThread
from buggarden.apps.forum.models import ForumReply


#---------------------------------------------------------------------------------------------------
# Constants
#---------------------------------------------------------------------------------------------------

THREADS_PER_PAGE      = 20
REPLIES_PER_PAGE      = 20

REQUEST_BOARD_ENABLED = True

CREATE_BOARD_FAILED_ERROR   = "CreateBoardFailed"

CREATE_BOARD_SUCCEEDED_INFO = "CreateBoardSucceeded"

ERROR_DICT = {
    CREATE_BOARD_FAILED_ERROR   : "Failed to create forum board.",
}

INFO_DICT = {
    CREATE_BOARD_SUCCEEDED_INFO : "Forum board created successfully.",
}

#---------------------------------------------------------------------------------------------------
# Common functions
#---------------------------------------------------------------------------------------------------

def _get_param(query_dict, name):
    value = query_dict.get(name, "").decode("utf8").strip()
    if len(value) == 0:
        return None
    else:
        return value


def _get_common_context_dict(request):
    user = users.get_current_user()
    if user is None:
        log_url  = users.create_login_url(request.path)
        log_text = "Login"
    else:
        log_url  = users.create_logout_url(request.path)
        log_text = "Logout"
    return { "user" : user, "log_url" : log_url, "log_text" : log_text, }


#---------------------------------------------------------------------------------------------------
# GET views
#---------------------------------------------------------------------------------------------------


def view_forum(request):
    # Find all the approved forum boards from datastore.
    boards = ForumBoard.gql( "WHERE status=:status ORDER BY create_date",
                             status=ForumBoard.APPROVED )
    # Count threads and posts for statistics.
    thread_count = ForumThread.gql("").count()
    post_count   = ForumReply.gql("").count() + thread_count
    # Render values to HttpResponse.
    vals = { "boards" : boards, "thread_count" : thread_count, "post_count" : post_count, }
    vals.update(_get_common_context_dict(request))
    ctxt = Context(vals)
    tmpl = loader.get_template("%s/view_forum.html" % Config.APP_NAME)
    return HttpResponse(tmpl.render(ctxt))


def view_board(request, id):
    board = ForumBoard.get_by_id(int(id))
    if board is None:
        # TODO
        pass
    else:
        # Retrieve threads in this board.
        query = ForumThread.gql( "WHERE board=:board ORDER BY last_post_date DESC", board=board )
        threads = query.fetch(THREADS_PER_PAGE, 0) # TODO
        # Render values to HttpResponse.
        vals = { "board" : board, "threads" : threads, }
        vals.update(_get_common_context_dict(request))
        ctxt = Context(vals)
        tmpl = loader.get_template("%s/view_board.html" % Config.APP_NAME)
        return HttpResponse(tmpl.render(ctxt))


def view_thread(request, id):
    error   = None
    thread  = None
    replies = None
    try:
        # Retrieve thread from datastore.
        thread = ForumThread.get_by_id(int(id))
        if thread is None:
            raise Exception("Cannot find ForumThread by id '%s'." % id)
        # Retrieve replies to this thread.
        query = ForumReply.gql("WHERE thread=:thread ORDER BY post_date", thread=thread)
        replies = query.fetch(REPLIES_PER_PAGE, 0) # TODO
    except Exception, ex:
        logging.error( "Failed to view thread (%s): %s" % (type(ex), str(ex)) )
        error = "Failed to view thread: %s" % str(ex)
    
    # Render to HttpResponse.
    vals = { "error" : error, "thread" : thread, "replies" : replies, }
    vals.update(_get_common_context_dict(request))
    ctxt = Context(vals)
    tmpl = loader.get_template("%s/view_thread.html" % Config.APP_NAME)
    return HttpResponse(tmpl.render(ctxt))


def get_attachment(request, id):
    try:
        attachment = ForumAttachment.get_by_id(int(id))
        if attachment is None:
            raise Exception("Attachment cannot be found by id '%s'." % id)
        resp = HttpResponse(attachment.data, mimetype=attachment.mime_type)
        resp["Content-Disposition"] = "attachment; filename=%s" % attachment.file_name
        return resp
    except Exception, ex:
        logging.error( "Fail to get attachment (%s): %s" % (type(ex), str(ex)) )
        return Http404("Attachment cannot be found by ID '%s'." % id)


#---------------------------------------------------------------------------------------------------
# GET/POST view functions
#---------------------------------------------------------------------------------------------------

def create_board(request):
    
    if users.get_current_user() is None:
        login_url = users.create_login_url(request.path)
        return HttpResponseRedirect(login_url)
        
    else:
        
        if request.method == "GET":
            # Render the create board form to HttpResponse.
            vals = _get_common_context_dict(request)
            ctxt = Context(vals)
            tmpl = loader.get_template("%s/create_board.html" % Config.APP_NAME)
            return HttpResponse(tmpl.render(ctxt))
        
        elif request.method == "POST":
            # Create a ForumBoard entity in the datastore.
            name = _get_param(request.POST, "name")
            desc = _get_param(request.POST, "desc")
            if name is not None:
                try:
                    board = ForumBoard( name=name,
                                        desc=desc,
                                        managers=[ users.get_current_user(), ] )
                    board.put()
                    params = { "info" : CREATE_BOARD_SUCCEEDED_INFO, "id" : board.key().id(), }
                except Exception, ex:
                    logging.error( "Fail to create ForumBoard (%s): %s" % (type(ex), str(ex)) )
                    params = { "error" : CREATE_BOARD_FAILED_ERROR, }
            else:
                params = { "error" : CREATE_BOARD_FAILED_ERROR, }
            # Redirect to the result URL.
            return HttpResponseRedirect( "%s/create_board_result/?%s"
                                       % (Config.URL_PREFIX, urllib.urlencode(params)) )
        
        else:
            # TODO
            pass


def create_board_result(request):
    """
    """
    
    # Get parameters from request.
    error = ERROR_DICT.get(_get_param(request.GET, "error"))
    info  = INFO_DICT.get( _get_param(request.GET, "info" ))
    id    = _get_param(request.GET, "id"   )
    # If id is available, get ForumBoard entity from datastore.
    board = None
    if id is not None:
        try:
            board = ForumBoard.get_by_id(int(id))
        except Exception, ex:
            logging.error( "Fail to get ForumBoard by id '%s' (%s): %s"
                         % (id, type(ex), str(ex)) )
    # Render to HttpResponse.
    vals = { "error" : error, "info" : info, "board" : board, }
    vals.update(_get_common_context_dict(request))
    ctxt = Context(vals)
    tmpl = loader.get_template("%s/create_board_result.html" % Config.APP_NAME)
    return HttpResponse(tmpl.render(ctxt))


def admin_forum(request):

    if not users.get_current_user() in Config.ADMINS:
        return HttpResponseRedirect("/forum/")
    
    else:
    
        if request.method == "GET":
        
            # Find all the forum boards from datastore.
            boards = ForumBoard.gql("ORDER BY create_date")
            # Render to HttpResponse.
            vals = { "boards" : boards, }
            vals.update(_get_common_context_dict(request))
            ctxt = Context(vals)
            tmpl = loader.get_template("%s/admin_forum.html" % Config.APP_NAME)
            return HttpResponse(tmpl.render(ctxt))
    
        elif request.method == "POST":
        
            # Retrieve user inputs from request.
            status    = _get_param(request.POST, "status")
            board_ids = request.POST.getlist("boards")
            if status is not None and board_ids is not None and len(board_ids) > 0:
                for id in board_ids:
                    try:
                        board = ForumBoard.get_by_id(int(id))
                        if board is not None:
                            board.status = status
                            board.put()
                    except Exception, ex:
                        logging.error( "Fail to update board with id '%s' (%s): %s"
                                     % (id, type(ex), str(ex)) )
            # Redirect to the admin page.
            return HttpResponseRedirect("/forum/admin/")
        
        else:
            # TODO
            pass


#---------------------------------------------------------------------------------------------------
# POST view functions
#---------------------------------------------------------------------------------------------------

def _add_attachment(request):
    
    # Check that the current user is authenticated.
    user = users.get_current_user()
    if user is None:
        raise Exception("The current user should be authenticated.")
    
    # If attachment is available in the request, save it to datastore.
    attachment_dict = request.FILES.get("attachment", None)
    if attachment_dict is not None:
        file_name = _get_param(attachment_dict, "filename")
        mime_type = _get_param(attachment_dict, "content-type")
        data      = attachment_dict.get("content")
        if file_name is None or mime_type is None or data is None:
            raise Exception("The uploaded attachment's information is incomplete.")
        attachment = ForumAttachment( file_name=file_name,
                                      mime_type=mime_type,
                                      data=data,
                                      uploader=user )
        attachment.put()
    else:
        attachment = None
    
    # Return the attachment, or None if no attachment is available in the request.
    return attachment


def add_thread_to_board(request, id):

    user = users.get_current_user()
    if user is not None and request.method == "POST":
        try:
            # Retrieve the forum board from datastore.
            board = ForumBoard.get_by_id(int(id))
            if board is None:
                raise Exception("Cannot find forum board by id '%s'." % id)
            # Add attachment to datastore if available.
            attachment = _add_attachment(request)
            # Add a new thread to the forum board.
            thread = ForumThread( board=board,
                                  title=_get_param(request.POST, "title"),
                                  content=_get_param(request.POST, "content"),
                                  attachment=attachment,
                                  author=user )
            thread.put()
        except Exception, ex:
            logging.error( "Fail to add thread to board (%s): %s." % (type(ex), str(ex)) )
    return HttpResponseRedirect("%s/board/%s" % (Config.URL_PREFIX, id))


def add_reply_to_thread(request, id):

    user = users.get_current_user()
    if user is not None and request.method == "POST":
        try:
            # Retrieve the forum thread from datastore.
            thread = ForumThread.get_by_id(int(id))
            if thread is None:
                raise Exception("Cannot find forum thread by id '%s'." % id)
            # Add attachment to datastore if available.
            attachment = _add_attachment(request)
            # Add a new reply to the forum thread.
            reply = ForumReply( thread=thread,
                                title=_get_param(request.POST, "title"),
                                content=_get_param(request.POST, "content"),
                                attachment=attachment,
                                author=user )
            reply.put()
        except Exception, ex:
            logging.error( "Fail to add reply to thread (%s): %s." % (type(ex), str(ex)) )
        
        vals = { "thread" : thread, "info" : "reply OK!", }
        vals.update(_get_common_context_dict(request))
        ctxt = Context(vals)
        tmpl = loader.get_template("%s/sections/thread.html" % Config.APP_NAME)
        return HttpResponse(tmpl.render(ctxt))
        # return HttpResponseRedirect("%s/thread/%s" % (Config.URL_PREFIX, id))


#---------------------------------------------------------------------------------------------------
# AJAX view functions
#---------------------------------------------------------------------------------------------------

def edit_thread(request, id):
    if request.method == "POST":
        error  = None
        info   = None
        thread = None
        try:
            # Get thread from datastore, and check that it is editable.
            thread = ForumThread.get_by_id(int(id))
            if thread is None:
                raise Exception("Cannot find ForumThread by id '%s'." % id)
            if not thread.is_current_user_author():
                raise Exception("Current user cannot edit ForumThread '%s'." % id)
            # Update thread and save it to datastore.
            thread.title     = _get_param(request.POST, "title")
            thread.content   = _get_param(request.POST, "content")
            thread.edit_date = datetime.datetime.today()
            thread.put()
            info = "Thread updated successfully."
        except Exception, ex:
            logging.error( "Failed to edit ForumThread in datastore (%s): %s"
                         % (type(ex), str(ex)) )
            error = "Failed to update thread: %s" % str(ex)
        
        vals = { "error" : error, "info" : info, "thread" : thread, }
        vals.update(_get_common_context_dict(request))
        ctxt = Context(vals)
        tmpl = loader.get_template("%s/sections/thread.html" % Config.APP_NAME)
        return HttpResponse(tmpl.render(ctxt))


def edit_reply(request, id):
    if request.method == "POST":
        error = None
        info  = None
        reply = None
        try:
            # Get reply from datastore, and check that it is editable.
            reply = ForumReply.get_by_id(int(id))
            if reply is None:
                raise Exception("Cannot find ForumReply by id '%s'." % id)
            if not reply.is_current_user_author():
                raise Exception("Current user cannot edit ForumReply '%s'." % id)
            # Update reply and save it to datastore.
            reply.title     = _get_param(request.POST, "title")
            reply.content   = _get_param(request.POST, "content")
            reply.edit_date = datetime.datetime.today()
            reply.put()
            info = "Reply updated successfully."
        except Exception, ex:
            logging.error( "Failed to edit ForumReply in datastore (%s): %s"
                         % (type(ex), str(ex)) )
            error = "Failed to update reply: %s" % str(ex)
        
        vals = { "error" : error, "info" : info, "reply" : reply, }
        vals.update(_get_common_context_dict(request))
        ctxt = Context(vals)
        tmpl = loader.get_template("%s/sections/reply.html" % Config.APP_NAME)
        return HttpResponse(tmpl.render(ctxt))


def recommend_thread(request, id):
    if request.method == "POST":
        error  = None
        info   = None
        thread = None
        try:
            thread = ForumThread.get_by_id(int(id))
            if thread is None:
                raise Exception("Cannot find ForumThread by id '%s'." % id)
            if not thread.board.is_current_user_manager():
                raise Exception("Current user cannot change status of ForumBoard '%s'." % id)
            days  = int(_get_param(request.POST, "days"))
            delta = datetime.timedelta(days)
            thread.recommended_till = datetime.datetime.today() + delta
            thread.put()
            info = "Thread status updated successfully."
        except Exception, ex:
            logging.error( "Failed to (un)recommend ForumThread in datastore (%s): %s"
                         % (type(ex), str(ex)) )
            error = "Failed to update thread status: %s" % str(ex)
        
        vals = { "error" : error, "info" : info, "thread" : thread, }
        vals.update(_get_common_context_dict(request))
        ctxt = Context(vals)
        tmpl = loader.get_template("%s/sections/thread.html" % Config.APP_NAME)
        return HttpResponse(tmpl.render(ctxt))


def lock_thread(request, id):
    if request.method == "POST":
        error  = None
        info   = None
        thread = None
        try:
            thread = ForumThread.get_by_id(int(id))
            if thread is None:
                raise Exception("Cannot find ForumThread by id '%s'." % id)
            if not thread.board.is_current_user_manager():
                raise Exception("Current user cannot change status of ForumThread '%s'." % id)
            lock = int(_get_param(request.POST, "lock"))
            thread.is_locked = bool(lock)
            thread.put()
            info = "Thread status updated successfully."
        except Exception, ex:
            logging.error( "Failed to (un)lock ForumThread in datastore (%s): %s"
                         % (type(ex), str(ex)) )
            error = "Failed to update thread status: %s" % str(ex)
        
        vals = { "error" : error, "info" : info, "thread" : thread, }
        vals.update(_get_common_context_dict(request))
        ctxt = Context(vals)
        tmpl = loader.get_template("%s/sections/thread.html" % Config.APP_NAME)
        return HttpResponse(tmpl.render(ctxt))


def delete_thread(request, id):
    if request.method == "POST":
        error = None
        info  = None
        try:
            # Get thread from datastore, and check that it is delete-able.
            thread = ForumThread.get_by_id(int(id))
            if thread is None:
                raise Exception("Cannot find ForumThread by id '%s'." % id)
            is_current_user_author  = thread.is_current_user_author()
            is_current_user_manager = thread.board.is_current_user_manager()
            if not is_current_user_author and not is_current_user_manager:
                raise Exception("Current user cannot delete ForumThread '%s'." % id);
            # Delete replies of this thread from datastore.
            # TODO: we need transaction here!
            replies = ForumReply.gql("WHERE thread=:thread", thread=thread)
            for reply in replies:
                reply.delete()
            # Delete thread from datastore.
            thread.delete()
            info = "Thread (and all its replies) deleted successfully."
        except Exception, ex:
            logging.error( "Failed to delete ForumReply or ForumThread in datastore (%s): %s"
                         % (type(ex), str(ex)) )
            error = "Failed to delete thread: %s" % str(ex)
        
        vals = { "error" : error, "info" : info, }
        vals.update(_get_common_context_dict(request))
        ctxt = Context(vals)
        tmpl = loader.get_template("%s/sections/thread.html" % Config.APP_NAME)
        return HttpResponse(tmpl.render(ctxt))


def delete_reply(request, id):
    if request.method == "POST":
        error = None
        info  = None
        try:
            # Get reply from datastore, and check that it is delete-able.
            reply = ForumReply.get_by_id(int(id))
            if reply is None:
                raise Exception( "Cannot find ForumReply by id '%s'." % id )
            is_current_user_author  = reply.is_current_user_author()
            is_current_user_manager = reply.thread.board.is_current_user_manager()
            if not is_current_user_author and not is_current_user_manager:
                raise Exception("Current user cannot delete ForumReply '%s'." % id);
            # Delete reply from datastore.
            reply.delete()
            info = "Reply deleted successfully."
        except Exception, ex:
            logging.error( "Failed to delete ForumReply in datastore (%s): %s"
                         % (type(ex), str(ex)) )
            error = "Failed to delete reply: %s" % str(ex)
        
        vals = { "error" : error, "info" : info, }
        vals.update(_get_common_context_dict(request))
        ctxt = Context(vals)
        tmpl = loader.get_template("%s/sections/reply.html" % Config.APP_NAME)
        return HttpResponse(tmpl.render(ctxt))


#---------------------------------------------------------------------------------------------------
# TODO:
#---------------------------------------------------------------------------------------------------




def admin_board(request, board_id):
    
    board = ForumBoard.get_by_id(int(board_id))
    if board is not None:
        
        if request.method == 'GET':
            values = { 'user'   : GoogleUser(request.path),
                       'board' : board, }
            ctxt = Context(values)
            tmpl = loader.get_template('%s/admin_board.html' % Config.APP_NAME)
            return HttpResponse(tmpl.render(ctxt))
        
        elif request.method == 'POST':
            
            form = ForumBoardForm()
            form.populate(request)
            board.description = form.description
            
            board.admins = []
            admin_emails = request.POST.get('admins', '').split(',')
            for email in admin_emails:
                email = email.strip()
                if len(email) > 0:
                    admin = users.User(email)
                    board.admins.append(admin)
            if len(board.admins) == 0:
                board.admins.append(user.get_current_user())
            
            board.put()
            
            return HttpResponseRedirect('/forum/board/%s/' % board_id)
            
        else:
            # TODO
            pass
    else:
        # TODO
        pass





